iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0

在操作外部資源時,會造成副作用,例如新增一筆資料到資料庫,就是一個包含副作用的操作。

實務上有個場景還滿常見,我們常常需要操作多個外部資源,只要其中一個環節失敗,我們就要對前面已經造成的副作用的操作做復原。手動做這些操作其實有點繁瑣惱人,所以我們 FP 工具 effect 也提供了解決這類問題的框架。

https://ithelp.ithome.com.tw/upload/images/20231014/20158615tXiUHPLmnH.png

以課程系統來舉例,這次我們想連通三種不同類型的資料服務,三個服務儲存的資料分別是課程的附檔文件或影片 (attachment)、新增課程的 Log (record),以及課程相關資訊 (meta)。當任意一個操作失敗時,都要對前面的操作做復原,所以我們所建立的服務除了提供新增功能以外,還要提供刪除功能。

export interface MinIo {
  createAttachment: Effect.Effect<never, MinIoError, Attachment>
  deleteAttachment: (
    attachment: Attachment
  ) => Effect.Effect<never, never, void>
}
const MinIo = { context: Context.Tag<MinIo>() }

export interface Elastic {
  createCourseRecord: (
    attachment: Attachment
  ) => Effect.Effect<never, ElasticError, CourseRecord>
  deleteCourseRecord: (
    record: CourseRecord
  ) => Effect.Effect<never, never, void>
}
const Elastic = { context: Context.Tag<Elastic>() }

export interface Mongo {
  createCourseMeta: (
    record: CourseRecord
  ) => Effect.Effect<never, MongoError, Course>
  deleteCourseMeta: (course: Course) => Effect.Effect<never, never, void>
}
const Mongo = { context: Context.Tag<Mongo>() }

AcquireRelease

接著我們用 Effect.acquireRelease 來定義要執行的動作跟失敗的動作

const tryCreateAttachment = pipe(
  MinIo.context,
  Effect.flatMap(({ createAttachment, deleteAttachment }) =>
    Effect.acquireRelease(
      // acquire
      createAttachment,
      //release
      (attachment, exit) =>
        Exit.isFailure(exit) ? deleteAttachment(attachment) : Effect.unit
    )
  )
)

const tryCreateCourseRecord = (attachment: Attachment) =>
  pipe(
    Elastic.context,
    Effect.flatMap(({ createCourseRecord, deleteCourseRecord }) =>
      Effect.acquireRelease(createCourseRecord(attachment), (record, exit) =>
        Exit.isFailure(exit) ? deleteCourseRecord(record) : Effect.unit
      )
    )
  )

const tryCreateCourseMeta = (record: CourseRecord) =>
  pipe(
    Mongo.context,
    Effect.flatMap(({ createCourseMeta, deleteCourseMeta }) =>
      Effect.acquireRelease(createCourseMeta(record), (record, exit) =>
        Exit.isFailure(exit) ? deleteCourseMeta(record) : Effect.unit
      )
    )
  )
type CreateAttachment = Effect.Effect<MinIo | Scope, MinIoError, Attachment>

把滑鼠移到 createAttachment 上可以看到它的型別多了一個之前沒看過的 Scope。這表示使用到 acquireReleaseeffect 都需要依賴 Scope 環境來運行。

Scope

那 ... 甚麼是 Scope 呢? 我們直接從 Scope 的用法說起

  • 你可以在 effect 盒子裡面新增一個 finalizer,來表示一連串 effect 操作結束後要執行的動作。
  • 你可以在 Scope 範圍內的一連串 effect 操作結束後選擇執行 finalizer

而說穿了,acquireRelease 就是一種特化的 Scope ,它保證只要 aquire 被執行,而且 Scope 內的各種 effect 操作也都執行完畢,release 就會被執行。實作上我們可以用以下方法把各種依賴 Scopeeffect 組合起來。

export const tryCreateCourse = Effect.scoped(
  pipe(
    tryCreateAttachment,
    Effect.flatMap(tryCreateCourseRecord),
    Effect.flatMap(tryCreateCourseMeta)
  )
)

最終就會拿到這樣的以下型別

Effect.Effect<MinIo | Elastic | Mongo, MinIoError | ElasticError | MongoError, Course>

表示我們只要集齊三個外部依賴,就可以在出錯也會復原的情況下新增課程。

Provide

最後我們再利用昨天的 Layer 技巧把上面的 effect 執行起來。

const layer = Layer.mergeAll(MinIoLayer, ElasticSearchLayer, MongoLayer)
// Layer<never, never, S3 | ElasticSearch | Database>

如何定義 Layer 昨天已經有說,今天就不再多占版面。

然後把 layer 丟給 tryCreateCourse ,最後再做個 runPromise 就能執行起來囉 !

pipe(
  tryCreateCourse,
  Effect.provide(layer),
  Effect.either, // 轉換成 either 型別避免報錯
  Effect.runPromise
)

上一篇
D28 - 管理依賴注入 (二)
下一篇
D30 - 總結
系列文
從 Next.js 開始的 Functional Programming30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言